﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.OleDb;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Xml;
using JasLib.Extensions;

namespace JasLib
{
    public static class GeneralUtilities
    {
        /***************************************************************************/
        /// <summary>
        /// http://stackoverflow.com/questions/16340/how-do-i-generate-a-hashcode-from-a-byte-array-in-c
        /// </summary>
        public static int ComputeHash(params byte[] data)
        {
            unchecked
            {
                const int p = 16777619;
                int hash = (int)2166136261;

                for (int i = 0; i < data.Length; i++)
                    hash = (hash ^ data[i]) * p;

                hash += hash << 13;
                hash ^= hash >> 7;
                hash += hash << 3;
                hash ^= hash >> 17;
                hash += hash << 5;
                return hash;
            }
        }

        /***************************************************************************/
        public static Microsoft.Win32.RegistryKey ParseStandardRegistryKeyName(string strKeyName)
        {
            string strValue = (strKeyName as string).TrimToEmpty().ToLower();
            switch (strValue)
            {
                case "hkcu":
                case "hkey_current_user":
                case "currentuser":
                return Microsoft.Win32.Registry.CurrentUser;
                case "hkcr":
                case "hkey_classes_root":
                case "classesroot":
                return Microsoft.Win32.Registry.ClassesRoot;
                case "hkcc":
                case "hkey_current_config":
                case "currentconfig":
                return Microsoft.Win32.Registry.CurrentConfig;
                case "hklm":
                case "hkey_local_machine":
                case "localmachine":
                return Microsoft.Win32.Registry.LocalMachine;
                case "hkpd":
                case "hkey_performance_data":
                case "performancedata":
                return Microsoft.Win32.Registry.PerformanceData;
                case "hku":
                case "hkey_users":
                case "users":
                return Microsoft.Win32.Registry.Users;
            }

            return null;
        }

        /***************************************************************************/
        public static Microsoft.Win32.RegistryKey CreateRegistryKeyFromFullPath(string strKeyPath)
        {
            Microsoft.Win32.RegistryKey key = null;

            List<string> components = strKeyPath.Split("\\", "/").WithoutWhiteSpace().ToList();
            if (!components.IsNullOrEmpty())
            {
                key = ParseStandardRegistryKeyName(components[0]);
                if (key != null && components.Count > 1)
                {
                    components.RemoveAt(0);
                    string strSubPath = components.Join("\\");
                    try
                    {
                        /// TODO: If you're specifying a sub-path, you might need full access to it.
                        key = key.CreateSubKey(strSubPath, Microsoft.Win32.RegistryKeyPermissionCheck.ReadSubTree);
                    }
                    catch
                    {
                        key = null;
                    }
                }
            }

            return key;
        }
        /**********************************************************************************/
        private static readonly string[] s_strByteCountUnitNames = { "bytes", "KB", "MB", "GB", "TB", "PB", "EB", };
        public static void FormatByteSize(
            ulong ulByteCount,
            out decimal units,
            out string strUnitName,
            uint uiDivisor = 1024)
        {
            /// The sole exception.
            if (ulByteCount == 1)
            {
                units = 1;
                strUnitName = "byte";
            }
            else
            {
                decimal divisor = (decimal)uiDivisor;

                units = ulByteCount;
                int iCurrentUnitIndex = 0;
                while (units > uiDivisor)
                {
                    units /= divisor;

                    iCurrentUnitIndex++;
                }

                strUnitName = s_strByteCountUnitNames[iCurrentUnitIndex];
            }

            return;
        }

        /**********************************************************************************/
        /// <summary>
        /// Make sure you COMPLETELY understand the performance and correctness implications
        /// of this function before using it.
        /// This may not be usable in release builds but is preserved here for education.
        /// </summary>
        public static string GetCallerPropertyName()
        {
            MethodBase method;

            /// Optimal scenario: the ViewModelBase functions are being called directly in the set-accessor.
            /// Frame 0: GetCallerPropertyName.
            /// Frame 1: SetProperty/OnPropertyChanging/OnPropertyChanged.
            /// Frame 2: set_PropertyNameGoesHere
            StackFrame stackFrame = new StackFrame(2, false);
            method = stackFrame.GetMethod();
            if ((method.Attributes & MethodAttributes.SpecialName) != 0)
            {
                string strName = method.Name;
                if (strName.StartsWith("set_"))
                    return strName.Substring(4);
            }

            /// Worst-case scenario: the ViewModelBase functions are being called in arbitrary functions.
            /// I have little or no pity here.
            StackFrame[] stackFrames = (new StackTrace()).GetFrames();
            for (int iIndex = 0; iIndex < stackFrames.Length; iIndex++)
            {
                method = stackFrames[iIndex].GetMethod();
                if ((method.Attributes & MethodAttributes.SpecialName) != 0)
                {
                    string strName = method.Name;
                    if (strName.StartsWith("set_"))
                        return strName.Substring(4);
                }
            }

            throw new InvalidOperationException("GetCallerPropertyName() was not called in the stack frame of a property's set-accessor.");
        }

        /**********************************************************************************/
        public static DataSet ImportExcelAsDataSet(string strFileName, bool bHasHeaders = true)
        {
            string strHasHeaders = bHasHeaders ? "Yes" : "No";

            OleDbConnectionStringBuilder builder = new OleDbConnectionStringBuilder
            {
                DataSource = strFileName,
            };

            if (Path.GetExtension(strFileName).ToLower() == ".xlsx")
            {
                builder.Provider = "Microsoft.ACE.OLEDB.12.0";
                builder["Extended Properties"] = "Excel 12.0;HDR={0};IMEX=1".FormatWith(strHasHeaders);
            }
            else
            {
                builder.Provider = "Microsoft.Jet.OLEDB.4.0";
                builder["Extended Properties"] = "Excel 8.0;HDR={0};IMEX=1".FormatWith(strHasHeaders);
            }

            DataSet dataset = new DataSet();

            using (OleDbConnection connection = new OleDbConnection(builder.ConnectionString))
            {
                connection.Open();

                DataTable schemaTable = connection.GetOleDbSchemaTable(OleDbSchemaGuid.Tables, new object[] { null, null, null, "TABLE" });

                foreach (DataRow schemaRow in schemaTable.Rows)
                {
                    string strWorksheet = schemaRow["TABLE_NAME"].ToString();

                    DataTable outputTable = new DataTable(strWorksheet);
                    dataset.Tables.Add(outputTable);

                    OleDbCommand cmd = new OleDbCommand("SELECT * FROM [{0}]".FormatWith(strWorksheet), connection)
                    {
                        CommandType = CommandType.Text,
                    };

                    new OleDbDataAdapter(cmd).Fill(outputTable);
                }
            }

            return dataset;
        }

        /**********************************************************************************/
        public static string PathJoin(
            bool bIncludeTrailingBackslash,
            params string[] components)
        {
            return components.PathJoin(@"\", bIncludeTrailingBackslash);
        }

        /**********************************************************************************/
        public static string PathJoin(
            this IEnumerable<string> components,
            string strSeparator,
            bool bIncludeTrailingSeparator)
        {
            StringBuilder sb = new StringBuilder();

            bool bFirst = true;
            foreach (string strComponent in components)
            {
                string strTrimmedComponent = strComponent.TrimToNull();
                if (strTrimmedComponent == null)
                    throw new ArgumentException("One of the component strings was null or empty.", "components");

                if (!bFirst)
                    sb.Append(strSeparator);

                int iFirst = 0;
                int iLast = strComponent.Length - 1;

                if (!bFirst && strTrimmedComponent.StartsWith(strSeparator))
                    iFirst += strSeparator.Length;
                if (strTrimmedComponent.EndsWith(strSeparator))
                    iLast -= strSeparator.Length;

                sb.Append(strTrimmedComponent.SubstringToIndex(iFirst, iLast));

                bFirst = false;
            }

            if (bIncludeTrailingSeparator)
                sb.Append(strSeparator);

            return sb.ToString();
        }

        /**********************************************************************************/
        public static async Task<MemoryStream> ReadAllFileBytesAsync(string strFilePath, CancellationToken cancellationToken = default(CancellationToken))
        {
            using (FileStream stream = new FileStream(strFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, 8, true))
            {
                int iBytesRead = 0;
                int iBytesRemaining = (int)stream.Length;
                var buffer = new byte[iBytesRemaining];

                int iThisReadSize = 0;

                while (iBytesRemaining > 0 &&
                    (iThisReadSize = await stream.ReadAsync(buffer, iBytesRead, iBytesRemaining)) != 0)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    iBytesRead += iThisReadSize;
                    iBytesRemaining -= iThisReadSize;
                }

                var memoryStream = new MemoryStream(buffer, 0, iBytesRead, true);

                /// If the file grew somehow, continue reading and stretch the memory stream (worst case).
                /*if (iThisReadSize > 0)
                {
                    memoryStream.Seek(0, SeekOrigin.End);
                    stream.CopyToSyncFromAsync(memoryStream, 4096, cancellationToken);
                }*/

                return memoryStream;
            }
        }

        /**********************************************************************************/
        public static object ReadXml(
            string strRawXml,
            Type type,
            IEnumerable<Type> knownTypes = null)
        {
            using (StringReader stringReader = new StringReader(strRawXml))
            using (XmlReader xmlReader = XmlReader.Create(stringReader))
            {
                DataContractSerializer serializer = new DataContractSerializer(type, knownTypes ?? Enumerable.Empty<Type>());
                return serializer.ReadObject(xmlReader);
            }
        }


        /**********************************************************************************/
        /// <summary>
        /// Uses <see cref="System.Runtime.Serialization.DataContractSerializer"/> to serialize
        /// the entire contents of an XML stream into one complete object graph of the given type.
        /// To be eligible for serialization, every object in the graph must either
        /// have the appropriate <see cref="System.Runtime.Serialization.DataContractAttribute"/>
        /// and <see cref="System.Runtime.Serialization.DataMemberAttribute"/> attributes
        /// or implement <see cref="System.Xml.Serialization.IXmlSerializable"/> (but not both).
        /// </summary>
        public static object ReadXmlFromBytes(
            MemoryStream memoryStream,
            Type type,
            IEnumerable<Type> knownTypes = null,
            Encoding encoding = null)
        {
            string strXml = (encoding ?? Encoding.UTF8).GetString(memoryStream.GetBuffer(), 0, (int)memoryStream.Length);
            return ReadXml(strXml, type, knownTypes);
        }

        /**********************************************************************************/
        public static void ReadXmlFromBytes<T>(
            MemoryStream memoryStream,
            out T objGraphRoot,
            IEnumerable<Type> knownTypes = null,
            Encoding encoding = null)
        {
            objGraphRoot = (T)ReadXmlFromBytes(memoryStream, typeof(T), knownTypes, encoding);
            return;
        }

        /**********************************************************************************/
        public static async Task<T> ReadXmlFileAsync<T>(
            string strPath,
            IEnumerable<Type> knownTypes = null,
            Encoding encoding = null)
        {
            string strNormalizedPath = Path.GetFullPath(strPath);
            var memoryStream = await ReadAllFileBytesAsync(strNormalizedPath);

            T value;
            ReadXmlFromBytes(memoryStream, out value, knownTypes ?? Enumerable.Empty<Type>(), encoding);
            return value;
        }

        /**********************************************************************************/
        private class VersionlessSerializationBinder : SerializationBinder
        {
            /// <summary>
            /// TODO: Implement caching.
            /// </summary>
            public override Type BindToType(string assemblyName, string typeName)
            {
                Type type = null;

                try
                {
                    type = Type.GetType("{0}, {1}".FormatWith(typeName, assemblyName));
                }
                catch
                {
                }
                finally
                {
                    if (type == null)
                    {
                        int iCommaIndex = assemblyName.IndexOf(',');
                        if (iCommaIndex != -1)
                        {
                            /// JAS: ¡BERMUDA TRIANGLE WARNING!
                            /// Do this right here w/o iCommaIndex and find out where the null exception comes from:
                            /// string strBareAssemblyName = assemblyName.Left(iCommaIndex + 1);

                            /// Include the comma.
                            string strBareAssemblyName = assemblyName.Left(iCommaIndex + 1);

                            AppDomain.CurrentDomain.AssemblyResolve += this.CurrentDomain_AssemblyResolve;
                            try
                            {
                                Assembly assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.FullName.StartsWith(strBareAssemblyName));
                                if (assembly != null)
                                {
                                    if (typeName.Contains("Version"))
                                    {
                                        var splitStr = typeName.Split(',');

                                        var strBuilder = new StringBuilder();
                                        splitStr.ForEach((x) =>
                                        {
                                            if (!x.Contains("Version"))
                                                strBuilder.Append(x + ",");
                                        });

                                        typeName = strBuilder.ToString().TrimEnd(',');                                        
                                    }

                                    type = assembly.GetType(typeName, true);
                                }
                            }
                            finally
                            {
                                AppDomain.CurrentDomain.AssemblyResolve -= this.CurrentDomain_AssemblyResolve;
                            }
                        }
                    }
                }

                return type;
            }

            private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
            {
                int iCommaIndex = args.Name.IndexOf(',');
                if (iCommaIndex != -1)
                {
                    /// Include the comma.
                    string strBareAssemblyName = args.Name.Left(iCommaIndex + 1);

                    Assembly assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.FullName.StartsWith(strBareAssemblyName));
                    if (assembly != null)
                    {
                        return assembly;
                    }
                }

                return null;
            }
        }

        /**********************************************************************************/
        public static object SerializeFromBytes(MemoryStream stream)
        {
            if (stream == null)
                return null;

            BinaryFormatter formatter = new BinaryFormatter
            {
                AssemblyFormat = FormatterAssemblyStyle.Simple,
                Binder = new VersionlessSerializationBinder(),
            };

            stream.Seek(0, SeekOrigin.Begin);
            return formatter.Deserialize(stream);
        }

        /**********************************************************************************/
        public static void SerializeFromBytes<T>(MemoryStream stream, out T value)
            where T : class
        {
            object objDeserialized = SerializeFromBytes(stream);
            if (objDeserialized != null && typeof(T).IsAssignableFrom(objDeserialized.GetType()))
                value = (T)objDeserialized;
            else
                value = null;
            return;
        }

        /**********************************************************************************/
        public static async Task<T> SerializeFromFileAsync<T>(string strFilePath)
            where T : class
        {
            if (File.Exists(strFilePath))
            {
                var memoryStream = await ReadAllFileBytesAsync(strFilePath);
                T value;
                SerializeFromBytes(memoryStream, out value);
                return value;
            }
            else
                return null;
        }

        /**********************************************************************************/
        /// <summary>
        /// </summary>
        /// <remarks>
        /// NOTE: Don't [de]serialize generics in .NET 3.5 or you won't be able to read them back if versions change
        /// (read http://support.microsoft.com/kb/960442).
        /// </remarks>
        public static MemoryStream SerializeToBytes(object obj)
        {
            if (obj == null)
                return null;

            MemoryStream memoryStream = new MemoryStream();

            BinaryFormatter formatter = new BinaryFormatter
            {
                AssemblyFormat = FormatterAssemblyStyle.Simple,
            };
            formatter.Serialize(memoryStream, obj);
            return memoryStream;
        }

        /**********************************************************************************/
        public static async Task SerializeToFileAsync(string strFilePath, object obj)
        {
            var memoryStream = SerializeToBytes(obj);
            await WriteAllFileBytesAsync(strFilePath, memoryStream);
            return;
        }

        /**********************************************************************************/
        public static void Swap<T>(ref T lhs, ref T rhs)
        {
            T temp = lhs;
            lhs = rhs;
            rhs = temp;
            return;
        }

        /**********************************************************************************/
        /// <summary>
        /// Identical semantics to Interlocked.Exchange() without locking the memory.
        /// </summary>
        public static T Swap<T>(ref T lhs, T rhs)
        {
            T temp = lhs;
            lhs = rhs;
            return temp;
        }

        /**********************************************************************************/
        public static TimeSpan TimeUnitsToTimeSpan(double units, TimeUnit timeUnit)
        {
            switch (timeUnit)
            {
                case TimeUnit.Days: return TimeSpan.FromDays(units);
                case TimeUnit.Hours: return TimeSpan.FromHours(units);
                case TimeUnit.Minutes: return TimeSpan.FromMinutes(units);
                case TimeUnit.Seconds: return TimeSpan.FromSeconds(units);
                case TimeUnit.Milliseconds: return TimeSpan.FromMilliseconds(units);
                case TimeUnit.Microseconds: return TimeSpan.FromMilliseconds(units / 1000);
                case TimeUnit.NtFileTimeTicks: return TimeSpan.FromMilliseconds(units / 100000);
                case TimeUnit.Nanoseconds: return TimeSpan.FromMilliseconds(units / 1000000);
                case TimeUnit.Picoseconds: return TimeSpan.FromMilliseconds(units / 100000000);
                default: throw new NotSupportedException("The provided unit {0} is either too cultural-subjective or not otherwise supported or implemented.".FormatWith(timeUnit));
            }
        }

        /************************************************************************************/
        public static DateTime UnixTimeToDateTimeUtc(long time_t)
        {
            return DateTime.FromFileTimeUtc(Interop.InteropUtilities.UnixTimeToFileTimeTicks(time_t));
        }

        /**********************************************************************************/
        public static async Task WriteAllFileBytesAsync(string strFilePath, MemoryStream memoryStream, CancellationToken cancellationToken = default(CancellationToken))
        {
            using (FileStream stream = new FileStream(strFilePath, FileMode.Create, FileAccess.Write, FileShare.Read))
            {
                await stream.WriteAsync(memoryStream.GetBuffer(), 0, (int)memoryStream.Length);
                return;
            }
        }

        /**********************************************************************************/
        /// <summary>
        /// Uses <see cref="System.Runtime.Serialization.DataContractSerializer"/> to serialize the provided object
        /// graph to its own XML stream.
        /// To be eligible for serialization, every object in the graph must either
        /// have the appropriate <see cref="System.Runtime.Serialization.DataContractAttribute"/>
        /// and <see cref="System.Runtime.Serialization.DataMemberAttribute"/> attributes
        /// or implement <see cref="System.Xml.Serialization.IXmlSerializable"/> (but not both).
        /// </summary>
        public static string WriteXml(object objGraphRoot)
        {
            StringBuilder sb = new StringBuilder();
            using (XmlWriter writer = XmlWriter.Create(sb))
            {
                DataContractSerializer serializer = new DataContractSerializer(objGraphRoot.GetType());
                serializer.WriteObject(writer, objGraphRoot);

                /// This is grievously necessary.
                /// http://social.msdn.microsoft.com/Forums/en-CA/wcf/thread/1a800237-f3d0-408f-be1d-c0555f3ba3af
                writer.Flush();

                return sb.ToString();
            }
        }

        /**********************************************************************************/
        public static byte[] WriteXmlToBytes(object objGraphRoot, Encoding encoding = null)
        {
            return (encoding ?? Encoding.UTF8).GetBytes(WriteXml(objGraphRoot));
        }

        /**********************************************************************************/
        /// <param name="strPath">The name of the file that will receive the XML stream.
        /// If this value is null, this method will write the data to a random new file in the Temp directory.</param>
        /// <returns>The full qualified path of the text file which received the XML stream.</returns>
        public static async Task<string> WriteXmlFileAsync(object objGraphRoot, string strPath = null)
        {
            string strNormalizedPath = strPath.IsNullOrWhiteSpace() ? Path.GetTempFileName() : Path.GetFullPath(strPath);
            var memoryStream = new MemoryStream(WriteXmlToBytes(objGraphRoot));
            await WriteAllFileBytesAsync(strNormalizedPath, memoryStream);
            return strNormalizedPath;
        }
    }
}
